﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.IdentityModel.Claims;
using System.IdentityModel.Policy;
using Sinacor.Infra.Common.Security.Authentication.Credentials;
using Sinacor.Infra.Service.Security.Authentication.ServiceCredentials.Providers;
using System.Security.Principal;
using Sinacor.Infra.Service.Security.Authorization;
using System.Net;
using Sinacor.Infra.Service.Security.Session;
using System.ServiceModel;
using System.ServiceModel.Channels;
using Sinacor.Infra.Common.Security.Authentication;
using PSE.Framework.ErrorLogging;

namespace Sinacor.Infra.Service.Security.Authentication.ServiceCredentials
{
    class SinacorTokenAuthenticator : SecurityTokenAuthenticator
    {
        protected override bool CanValidateTokenCore(System.IdentityModel.Tokens.SecurityToken token)
        {
            return (token is SinacorToken);
        }

        /// <summary>
        /// Executa a validação do Token passado.
        /// </summary>
        /// <param name="token">SecurityToken</param>
        /// <returns>Lista de politicas de acesso.</returns>
        protected override System.Collections.ObjectModel.ReadOnlyCollection<System.IdentityModel.Policy.IAuthorizationPolicy> ValidateTokenCore(System.IdentityModel.Tokens.SecurityToken token)
        {
            try
            {
                ISecurityTokenAuthenticationProvider securityTokenAuthentication = null;

                securityTokenAuthentication = SecurityTokenAutenticationProviderFactory.RetrieveProvider();

                if (!securityTokenAuthentication.ValidateToken(token))
                    throw new SecurityTokenValidationException("Invalid token");

                SinacorToken sinacorToken = token as SinacorToken;

                if (sinacorToken != null)
                {
                    return HandleSinacorToken(sinacorToken);
                }
                else
                {
                    return null;
                }
            }
            catch (Exception ex)
            {
                LogType logType = LogType.Error;

                if (ex is SecurityTokenValidationException &&
                    ex.Message == "Invalid token")
                    logType = LogType.Warning;

                Dictionary<string, object> extensionData = null;

                if (token is SinacorToken &&
                    ((SinacorToken)token).UserInfo != null &&
                    !string.IsNullOrEmpty(((SinacorToken)token).UserInfo.UserName))
                {
                    extensionData = new Dictionary<string, object>() { { "SinacorUserName", ((SinacorToken)token).UserInfo.UserName } };
                }

                if (ex.Data != null && ex.Data.Count > 0)
                {
                    extensionData = extensionData ?? new Dictionary<string, object>();
                    foreach (string key in ex.Data.Keys)
                    {
                        extensionData.Add(key, ex.Data[key]);
                    }
                }

                ErrorLogger.WriteLog(logType, ex, extensionData);

                if (ex is SecurityTokenValidationException &&
                    ex.Message == "Invalid token")
                    throw new FaultException(ex.Message, new FaultCode("InvalidToken"));
                else
                    throw;
            }
        }

        /// <summary>
        /// Manipula um SinacorToken e retorna a lista de AuthorizationPolicies.
        /// </summary>
        /// <param name="sinacorToken"></param>
        /// <returns></returns>
        private System.Collections.ObjectModel.ReadOnlyCollection<IAuthorizationPolicy> HandleSinacorToken(SinacorToken sinacorToken)
        {
            List<IAuthorizationPolicy> policies = new List<IAuthorizationPolicy>();
            ClaimSet claimSet = null;

            //Caso uma sessão tenha sido passada, verifica se é uma sessão válida.
            if (sinacorToken.UserInfo.SessionId != null && sinacorToken.UserInfo.SessionId.Value != 0 && sinacorToken.UserInfo.SessionExpires)
            {
                claimSet = RenewSession(sinacorToken);
            }
            else
                //Caso contrário, cria uma sessão para o usuário.
                claimSet = CreateSession(sinacorToken);

            policies.Add(new SinacorAuthorizationPolicy(claimSet));

            return policies.AsReadOnly();
        }

        private ClaimSet RenewSession(SinacorToken sinacorToken)
        {
            //try
            //{
            SessionControl sc = new SessionControl();
            int userId = RetrieveUserId(sinacorToken.UserInfo.UserName);

            //Cria uma lista com as claims para o usuário.
            List<Claim> claimList = CreateClaimList(sinacorToken.UserInfo.SessionId.Value, userId, sinacorToken.UserInfo);

            //Retorna a claim de sessão
            return new DefaultClaimSet(ClaimSet.System, claimList);
            //}
            //catch (Exception ex)
            //{
            //    //TODO: Adicionar tratamento de log.
            //    //System.Diagnostics.EventLog.WriteEntry("RenewSession", ex.Message);
            //    //Disparar uma InvalidSessionException.
            //    throw new Exception("ValidateSession ->" + ex.Message, ex);
            //}
        }

        /// <summary>
        /// Cria uma sessão para o token autenticado
        /// e retorna um Claim definindo o Id do usuário.
        /// </summary>
        /// <param name="token">SinacorToken Autenticado</param>
        /// <returns>ClaimSet com a lista de Claims de Autenticação/Autorização</returns>
        private ClaimSet CreateSession(SinacorToken token)
        {
            //try
            //{
            SessionControl sc = new SessionControl();
            int userId = RetrieveUserId(token.UserInfo.UserName);

            if (token.UserInfo.CompanyId == 0 && token.UserInfo.SessionExpires)
                //Remove todas as sessões abertas para o usuário
                sc.RemoveByUserId(userId);
            else
                if (token.UserInfo.SessionExpires)
                {
                    //Remove qualquer sessão aberta para o usuário/empresa.
                    sc.RemoveByUserId(userId, token.UserInfo.CompanyId);
                }

            //Cria uma nova sessão para o usuário
            SessionData sessionData = sc.Create(userId, token.UserInfo.CompanyId, RetrieveIpAddressFromMessage());

            //Cria uma lista com as claims para o usuário.
            List<Claim> claimList = CreateClaimList(sessionData.Id, userId, token.UserInfo);

            //Retorna a claim de sessão
            return new DefaultClaimSet(ClaimSet.System, claimList);
            //}
            //catch (Exception ex)
            //{
            //    //TODO: Adicionar tratamento de log.
            //    //System.Diagnostics.EventLog.WriteEntry("CreateSession", ex.Message);
            //    //Disparar uma InvalidSessionException.
            //    throw new Exception("CreateSession ->" + ex.Message, ex);
            //}
        }

        /// <summary>
        /// Cria a lista com as Claims do usuário.
        /// </summary>
        /// <param name="sessionId">Id da sessão</param>
        /// <param name="userId">Id do usuário</param>
        /// <param name="userName">Login do usuário</param>
        /// <param name="companyId">Id da empresa</param>
        /// <returns></returns>
        private List<Claim> CreateClaimList(int sessionId, int userId, SinacorUserInfo userInfo)
        {
            List<Claim> result = new List<Claim>();

            //Cria um claim contendo a sessão criada.
            result.Add(new Claim(SinacorClaimTypes.Identity.SessionId, sessionId.ToString(), Rights.PossessProperty));
            //Cria uma claim contendo o código do usuário
            result.Add(new Claim(SinacorClaimTypes.Identity.UserId, userId.ToString(), Rights.PossessProperty));
            //Cria uma claim contendo o login do usuário
            result.Add(new Claim(SinacorClaimTypes.Identity.LoginName, userInfo.UserName, Rights.PossessProperty));
            //Cria uma claim contendo o password cifrado do usuário
            result.Add(new Claim(SinacorClaimTypes.Identity.Password, userInfo.Password, Rights.PossessProperty));
            //Cria uma claim contendo o id do token
            result.Add(new Claim(SinacorClaimTypes.Identity.TokenId, userInfo.TokenId, Rights.PossessProperty));
            //Cria uma claim contendo o código da empresa
            result.Add(new Claim(SinacorClaimTypes.Identity.CompanyId, userInfo.CompanyId.ToString(), Rights.PossessProperty));
            //Cria uma claim contendo o tipo de sessão
            result.Add(new Claim(SinacorClaimTypes.Identity.SessionExpires, userInfo.SessionExpires.ToString(), Rights.PossessProperty));

            return result;

        }

        /// <summary>
        /// Recupera o código do usuário, a partir do login.
        /// </summary>
        /// <param name="login"></param>
        /// <returns></returns>
        public static int RetrieveUserId(string login)
        {
            AccessControl ac = new AccessControl();
            User user = ac.RetrieveUser(login);
            if (null != user)
                return user.Id;

            throw new Exception("An error ocurred trying to get UserId");
        }

        /// <summary>
        /// Recupera o Ip do requisitante pelo contexto da Mensagem
        /// </summary>
        /// <returns></returns>
        private static string RetrieveIpAddressFromMessage()
        {
            string clientHost = "NotFound";

            OperationContext operation = OperationContext.Current;

            if (operation != null)
            {
                MessageHeaders headers = operation.IncomingMessageHeaders;
                int headerIndex = headers.FindHeader(AuthenticationHeader.Name, AuthenticationHeader.NameSpace);

                if (headerIndex >= 0)
                {
                    AuthenticationHeader header = headers.GetHeader<AuthenticationHeader>(headerIndex);
                    if (header != null)
                        if (string.IsNullOrEmpty(header.ClientIp))
                            clientHost = header.ClientIp;
                        else
                            clientHost = header.ClientHostName;
                }
            }

            return clientHost;
        }
    }



}
